Design Inventory Management System

Ashish

Ashish Pratap Singh

medium

In this chapter, we will explore the low-level design of an inventory management system in detail.

Lets start by clarifying the requirements:

1. Clarifying Requirements

We are tasked with designing a system to manage inventory across multiple warehouses. The system should track products, stock levels, and handle orders.

Let's begin by clearly defining the system's capabilities.

With these clarifications, we can now summarize the key system requirements.

1.1 Functional Requirements

  • Support basic inventory operations: add new items, update quantities, and remove stock.
  • Maintain stock levels per product, per warehouse.
  • Allow multiple warehouses, each with its own inventory tracking.
  • Enable setting and checking minimum stock thresholds for alerts.
  • Record a history of all inventory transactions, including timestamps and operation types.
  • Support viewing current stock levels by product and by warehouse.

1.2 Non-Functional Requirements

  • Modularity: The system should follow object-oriented principles with well-separated components.
  • Consistency: Inventory updates should be accurate and reflect immediately across relevant views and reports.
  • Thread-Safety: The system must handle concurrent updates safely, especially when modifying stock quantities.
  • Extensibility: The design should support future enhancements like batch imports, barcoding, or serial number tracking.
  • Auditability: All operations should be logged for traceability and future analysis.
  • Maintainability: The code should be clean, testable, and easy to enhance.

With the requirements clarified, the next step is to identify the core entities and responsibilities in the system.

2. Identifying Core Entities

Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and extracting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.

Let’s walk through the functional requirements and extract the relevant entities:

1. The system must support basic inventory operations such as adding, removing, and updating stock.

This suggests the need for an InventoryService or InventoryManager component that provides APIs to perform operations on stock. Each operation should be tied to a specific product and warehouse.

2. Inventory is tracked per product, per warehouse.

This indicates the need for a Warehouse entity that holds an independent stock record, and a Product entity that represents the items being tracked. A mapping of product-to-quantity should be maintained within each warehouse.

3. Each product should have associated metadata such as name, ID, and description.

This validates the need for a well-defined Product entity to encapsulate product-specific information.

4. Each warehouse maintains stock levels independently.

The Warehouse entity will be responsible for managing a collection of product stock entries and exposing methods to query and update them.

5. The system should track inventory operations for audit and reporting.

This introduces a InventoryLog entity that records each inventory operation (e.g., ADD, REMOVE, UPDATE), including metadata such as timestamp, quantity, product ID, warehouse, and operation type.

6. Users should be able to set minimum stock thresholds and receive alerts when stock falls below those levels.

This suggests a StockThreshold entity or a threshold field associated with each product-warehouse pair. The InventoryManager will periodically or reactively check stock levels against the threshold and trigger an alert when necessary.

7. The system should handle concurrent inventory updates safely.

While not a separate entity, this requirement influences the design of InventoryManager, which must ensure thread-safe updates, possibly through synchronization or locking mechanisms.

These core entities form the foundational abstractions of an inventory management system. They will guide the class structure, data flow, and system responsibilities, ensuring the design is robust, extensible, and easy to maintain.

3. Designing Classes and Relationships

This section details the classes that form the core of the inventory management system, their relationships, and the key design patterns employed to ensure a robust, scalable, and maintainable architecture.

3.1 Class Definitions

The system is designed around a set of well-defined classes, each with a single responsibility. They can be categorized as Enums, Data Classes, and Core Classes.

Enums

TransactionType

A simple enumeration to categorize an inventory transaction.

TransactionType

This improves type safety and code readability compared to using raw strings or integers.

  • ADD: Represents an increase in stock.
  • REMOVE: Represents a decrease in stock.
  • INITIAL_STOCK: Represents the first stock entry when a product is introduced.

Data Classes

Product

An immutable class representing a stockable item.

Product

It contains essential details like productId, name, and description. Its immutability is enforced by a private constructor and the absence of setters, making it inherently thread-safe. It is constructed using the Builder pattern.

Transaction

An immutable record representing a single, auditable event in the inventory log.

Transaction

It captures the "what, when, where, and how much" of a stock change, including a unique transactionId, timestamp, productId, warehouseId, quantityChange, and TransactionType.

Core Classes

InventoryManager

The central Singleton and Facade for the system.

InventoryManager

It provides a simplified, high-level API for all client interactions, such as adding stock or viewing inventory. It orchestrates all operations between warehouses, products, and the audit service.

Warehouse

Represents a physical location that stores inventory.

Warehouse

It manages a collection of StockItem objects, mapping each product to its specific stock details within that warehouse.

StockItem

StockItem

A crucial class that encapsulates the inventory details for a single product within a single warehouse.

It holds the quantity, tracks the low-stock threshold, and acts as the Subject in the Observer pattern, notifying observers of any stock updates.

AuditService

AuditService

A Singleton service responsible for maintaining a complete, thread-safe log of all Transactions. It provides a central point for recording and retrieving audit trails.

3.2 Class Relationships

The relationships between classes define the system's structure and data flow.

Composition (Strong "has-a" relationship)

  • InventoryManager ◆— Warehouse: The InventoryManager holds and manages the lifecycle of all Warehouse instances.
  • Warehouse ◆— StockItem: A Warehouse is composed of multiple StockItems. A StockItem cannot exist without a parent Warehouse.
  • StockItem ◆— Product: A StockItem represents the stock of one specific Product.

Association (Weaker "uses-a" relationship):

  • StockItem — StockObserver: A StockItem (the subject) holds a list of StockObservers to notify upon state change. This is a one-to-many association.
  • InventoryManager — AuditService: The manager uses the single instance of the AuditService to log transactions.

Implementation (Is-a relationship):

  • LowStockAlertObserver --â–· StockObserver: The LowStockAlertObserver is a concrete implementation of the StockObserver interface.

Dependency (Transitive "uses" relationship):

  • InventoryManager ---> Transaction: The manager creates Transaction objects and passes them to the AuditService.
  • ProductFactory ---> Product.ProductBuilder: The factory depends on the builder to construct Product objects.

3.3 Key Design Patterns

Several design patterns are strategically used to achieve flexibility, safety, and separation of concerns.

Observer Pattern

StockObserver

Implemented with StockItem as the Subject and StockObserver as the Observer. When the stock level in a StockItem changes, it automatically notifies all its registered observers (LowStockAlertObserver), which then react to the change. This decouples the stock-taking logic from the alerting mechanism.

Builder Pattern

ProductBuilder

The Product class uses a static inner ProductBuilder to create its instances. This pattern is ideal for constructing complex, immutable objects. It improves readability by allowing a step-by-step construction process and ensures object validity before the final object is created.

Factory Pattern (Simple Factory)

ProductFactory

The ProductFactory class serves as a simple factory. It encapsulates the instantiation logic of Product objects, hiding the complexity of the ProductBuilder from the client and providing a straightforward creation method.

Singleton Pattern

Used for InventoryManager and AuditService. This ensures that there is only one instance of these central components throughout the application, providing a global point of access and control over the system's state and audit log.

Facade Pattern

The InventoryManager acts as a facade. It provides a simple, unified interface (addStock, removeStock) that hides the complex interactions between Warehouse, StockItem, AuditService, and StockObservers. Clients interact with this simple API without needing to know the internal details.

3.4 Full Class Diagram

Inventory Management System Class Diagram

4. Implementation

4.1 TransactionType Enum

Defines the type of inventory transaction

1class TransactionType(Enum):
2    ADD = "ADD"
3    REMOVE = "REMOVE"
4    INITIAL_STOCK = "INITIAL_STOCK"
  • ADD: Increase in stock
  • REMOVE: Decrease in stock
  • INITIAL_STOCK: Setup stock added when a product is first introduced to a warehouse

4.2 Product and Builder

Represents an item that can be stocked in warehouses. Built using the Builder pattern to enforce validation and flexibility during creation.

1class Product:
2    def __init__(self, builder: 'ProductBuilder'):
3        self._product_id = builder._product_id
4        self._name = builder._name
5        self._description = builder._description
6    
7    def get_product_id(self) -> str:
8        return self._product_id
9    
10    def get_name(self) -> str:
11        return self._name
12    
13    def get_description(self) -> str:
14        return self._description
15    
16    def __str__(self) -> str:
17        return f"Product{{id='{self._product_id}', name='{self._name}'}}"
18    
19    class ProductBuilder:
20        def __init__(self, product_id: str):
21            self._product_id = product_id
22            self._name: Optional[str] = None
23            self._description: Optional[str] = None
24        
25        def with_name(self, name: str) -> 'Product.ProductBuilder':
26            self._name = name
27            return self
28        
29        def with_description(self, description: str) -> 'Product.ProductBuilder':
30            self._description = description
31            return self
32        
33        def build(self) -> 'Product':
34            if self._name is None or self._name.strip() == "":
35                raise ValueError("Product name cannot be null or empty.")
36            return Product(self)

4.3 ProductFactory

Provides a convenient static method to construct validated Product objects using the builder internally.

1class ProductFactory:
2    @staticmethod
3    def create_product(product_id: str, name: str, description: str) -> Product:
4        return Product.ProductBuilder(product_id) \
5            .with_name(name) \
6            .with_description(description) \
7            .build()

4.4 StockItem

Encapsulates product-level inventory for a specific warehouse. A StockItem is responsible for one product in one warehouse. This includes its current quantity and the low-stock threshold.

1class StockItem:
2    def __init__(self, product: Product, quantity: int, threshold: int, warehouse_id: int):
3        self._product = product
4        self._quantity = quantity
5        self._threshold = threshold
6        self._warehouse_id = warehouse_id
7        self._observers: List[StockObserver] = []
8        self._lock = threading.Lock()
9    
10    def get_product(self) -> Product:
11        return self._product
12    
13    def get_quantity(self) -> int:
14        return self._quantity
15    
16    def get_threshold(self) -> int:
17        return self._threshold
18    
19    def get_warehouse_id(self) -> int:
20        return self._warehouse_id
21    
22    def add_observer(self, observer: StockObserver):
23        self._observers.append(observer)
24    
25    def remove_observer(self, observer: StockObserver):
26        if observer in self._observers:
27            self._observers.remove(observer)
28    
29    def update_stock(self, quantity_change: int) -> bool:
30        with self._lock:
31            if self._quantity + quantity_change < 0:
32                print(f"Cannot remove more stock than available. "
33                      f"Available: {self._quantity}, Attempted to remove: {-quantity_change}")
34                return False
35            
36            self._quantity += quantity_change
37            print(f"Stock updated for {self._product.get_name()} in Warehouse {self._warehouse_id}. "
38                  f"New quantity: {self._quantity}")
39            self._notify_observers()
40            return True
41    
42    def _notify_observers(self):
43        for observer in self._observers:
44            observer.on_stock_update(self)

Implements the Observer pattern to alert when stock falls below threshold. The updateStock() method ensures thread-safe inventory changes and notifies all observers.

4.5 Warehouse

A warehouse represents a physical location that holds stock.

1class Warehouse:
2    def __init__(self, warehouse_id: int, location: str):
3        self._warehouse_id = warehouse_id
4        self._location = location
5        self._stock_items: Dict[str, StockItem] = {}
6    
7    def get_warehouse_id(self) -> int:
8        return self._warehouse_id
9    
10    def get_location(self) -> str:
11        return self._location
12    
13    def add_product_stock(self, stock_item: StockItem):
14        self._stock_items[stock_item.get_product().get_product_id()] = stock_item
15    
16    def update_stock(self, product_id: str, quantity_change: int) -> bool:
17        stock_item = self._stock_items.get(product_id)
18        if stock_item is not None:
19            return stock_item.update_stock(quantity_change)
20        else:
21            print(f"Error: Product {product_id} not found in warehouse {self._warehouse_id}")
22            return False
23    
24    def get_stock_level(self, product_id: str) -> int:
25        stock_item = self._stock_items.get(product_id)
26        return stock_item.get_quantity() if stock_item is not None else 0
27    
28    def print_inventory(self):
29        print(f"--- Inventory for Warehouse {self._warehouse_id} ({self._location}) ---")
30        if not self._stock_items:
31            print("Warehouse is empty.")
32            return
33        
34        for item in self._stock_items.values():
35            print(f"Product: {item.get_product().get_name()} ({item.get_product().get_product_id()}), "
36                  f"Quantity: {item.get_quantity()}")
37        print("-------------------------------------------------")

4.6 Transaction

Models an audit log entry for stock change operations.

1class Transaction:
2    def __init__(self, product_id: str, warehouse_id: int, quantity_change: int, transaction_type: TransactionType):
3        self._transaction_id = str(uuid.uuid4())
4        self._timestamp = datetime.now()
5        self._product_id = product_id
6        self._warehouse_id = warehouse_id
7        self._quantity_change = quantity_change
8        self._type = transaction_type
9    
10    def __str__(self) -> str:
11        return (f"Transaction [ID={self._transaction_id}, Time={self._timestamp}, "
12                f"Warehouse={self._warehouse_id}, Product={self._product_id}, "
13                f"Type={self._type.value}, QtyChange={self._quantity_change}]")

4.7 AuditService

Tracks all inventory transactions in a thread-safe manner.

1class AuditService:
2    _instance = None
3    _lock = threading.Lock()
4    
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12    
13    def __init__(self):
14        if not self._initialized:
15            self._transaction_log: List[Transaction] = []
16            self._initialized = True
17    
18    @classmethod
19    def get_instance(cls):
20        return cls()
21    
22    def log(self, transaction: Transaction):
23        self._transaction_log.append(transaction)
24    
25    def print_audit_log(self):
26        print("\n--- Audit Log ---")
27        for transaction in self._transaction_log:
28            print(transaction)
29        print("-----------------")

4.8 StockObserver

Defines observers that react to stock changes.

1class StockObserver(ABC):
2    @abstractmethod
3    def on_stock_update(self, stock_item: 'StockItem'):
4        pass
5
6class LowStockAlertObserver(StockObserver):
7    def on_stock_update(self, stock_item: 'StockItem'):
8        if stock_item.get_quantity() < stock_item.get_threshold():
9            print(f"ALERT: Low stock for {stock_item.get_product().get_name()} in warehouse "
10                  f"{stock_item.get_warehouse_id()}. Current quantity: {stock_item.get_quantity()}, "
11                  f"Threshold: {stock_item.get_threshold()}")

LowStockAlertObserver emits a warning when the available stock falls below a predefined threshold.

4.9 InventoryManager (Singleton)

This class is the central controller for the entire system, providing a simplified API for clients.

1class InventoryManager:
2    _instance = None
3    _lock = threading.Lock()
4    
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10                    cls._instance._initialized = False
11        return cls._instance
12    
13    def __init__(self):
14        if not self._initialized:
15            self._products: Dict[str, Product] = {}
16            self._warehouses: Dict[int, Warehouse] = {}
17            self._audit_service = AuditService.get_instance()
18            self._initialized = True
19    
20    @classmethod
21    def get_instance(cls):
22        return cls()
23    
24    def add_warehouse(self, warehouse_id: int, location: str) -> Warehouse:
25        warehouse = Warehouse(warehouse_id, location)
26        self._warehouses[warehouse_id] = warehouse
27        return warehouse
28    
29    def add_product(self, product: Product):
30        self._products[product.get_product_id()] = product
31    
32    def add_product_to_warehouse(self, product_id: str, warehouse_id: int, initial_quantity: int, threshold: int):
33        warehouse = self._warehouses.get(warehouse_id)
34        product = self._products.get(product_id)
35        
36        if warehouse is None or product is None:
37            print("Warehouse or product not found")
38            return
39        
40        stock_item = StockItem(product, initial_quantity, threshold, warehouse_id)
41        stock_item.add_observer(LowStockAlertObserver())  # Register the observer
42        warehouse.add_product_stock(stock_item)
43        
44        # Log the initial stock
45        self._audit_service.log(Transaction(product.get_product_id(), warehouse_id, 
46                                          initial_quantity, TransactionType.INITIAL_STOCK))
47    
48    def _update_stock(self, warehouse_id: int, product_id: str, quantity_change: int):
49        warehouse = self._warehouses.get(warehouse_id)
50        
51        if warehouse is None:
52            print(f"Error: Warehouse {warehouse_id} not found.")
53            return
54        
55        success = warehouse.update_stock(product_id, quantity_change)
56        
57        if success:
58            transaction_type = TransactionType.ADD if quantity_change >= 0 else TransactionType.REMOVE
59            self._audit_service.log(Transaction(product_id, warehouse_id, quantity_change, transaction_type))
60    
61    def add_stock(self, warehouse_id: int, product_id: str, quantity: int):
62        self._update_stock(warehouse_id, product_id, quantity)
63    
64    def remove_stock(self, warehouse_id: int, product_id: str, quantity: int):
65        self._update_stock(warehouse_id, product_id, -quantity)
66    
67    def view_inventory(self, warehouse_id: int):
68        warehouse = self._warehouses.get(warehouse_id)
69        if warehouse is not None:
70            warehouse.print_inventory()
71        else:
72            print(f"Warehouse with ID {warehouse_id} not found.")
73    
74    def view_audit_log(self):
75        self._audit_service.print_audit_log()
  • Singleton and Facade: The InventoryManager is a Singleton that also acts as a Facade. It provides a simple, high-level API (addStock, removeStock) that hides the internal complexity of dealing with warehouses, stock items, observers, and audit logs.
  • Orchestration: Its methods orchestrate the interactions between different components. For example, addProductToWarehouse is responsible for creating a StockItem, registering an observer with it, adding it to the correct Warehouse, and logging the initial stock transaction with the AuditService.

4.10 InventoryManagementDemo

This driver class demonstrates the end-to-end functionality of the system from a client's perspective.

1class InventoryManagementDemo:
2  @staticmethod
3  def main():
4      # Get the singleton instance of the InventoryManager
5      inventory_manager = InventoryManager.get_instance()
6      
7      # 1. Setup: Add warehouses and products
8      warehouse1 = inventory_manager.add_warehouse(1, "New York")
9      warehouse2 = inventory_manager.add_warehouse(2, "San Francisco")
10      
11      laptop = ProductFactory.create_product("P001", "Dell XPS 15", "A high-performance laptop")
12      mouse = ProductFactory.create_product("P002", "Logitech MX Master 3", "An ergonomic wireless mouse")
13      
14      inventory_manager.add_product(laptop)
15      inventory_manager.add_product(mouse)
16      
17      # 2. Add initial stock to warehouses
18      print("--- Initializing Stock ---")
19      inventory_manager.add_product_to_warehouse(laptop.get_product_id(), warehouse1.get_warehouse_id(), 10, 5)  # 10 laptops in NY, threshold 5
20      inventory_manager.add_product_to_warehouse(mouse.get_product_id(), warehouse1.get_warehouse_id(), 50, 20)   # 50 mice in NY, threshold 20
21      inventory_manager.add_product_to_warehouse(laptop.get_product_id(), warehouse2.get_warehouse_id(), 8, 3)    # 8 laptops in SF, threshold 3
22      print()
23      
24      # 3. View initial inventory
25      inventory_manager.view_inventory(warehouse1.get_warehouse_id())
26      inventory_manager.view_inventory(warehouse2.get_warehouse_id())
27      
28      # 4. Perform stock operations
29      print("\n--- Performing Stock Operations ---")
30      inventory_manager.add_stock(warehouse1.get_warehouse_id(), laptop.get_product_id(), 5)    # Add 5 laptops to NY
31      inventory_manager.remove_stock(warehouse1.get_warehouse_id(), mouse.get_product_id(), 35)  # Remove 35 mice from NY -> should trigger alert
32      inventory_manager.remove_stock(warehouse2.get_warehouse_id(), laptop.get_product_id(), 6)  # Remove 6 laptops from SF -> should trigger alert
33      
34      # 5. Demonstrate error case: removing too much stock
35      print("\n--- Demonstrating Insufficient Stock Error ---")
36      inventory_manager.remove_stock(warehouse2.get_warehouse_id(), laptop.get_product_id(), 100)  # Fails, only 2 left
37      print()
38      
39      # 6. View final inventory
40      print("\n--- Final Inventory Status ---")
41      inventory_manager.view_inventory(warehouse1.get_warehouse_id())
42      inventory_manager.view_inventory(warehouse2.get_warehouse_id())
43      
44      # 7. View the full audit log
45      inventory_manager.view_audit_log()
46
47if __name__ == "__main__":
48    InventoryManagementDemo.main()

5. Run and Test

Languages
Java
C#
Python
C++
Files11
entities
enum
factory
observers
service
inventory_management_demo.py
main
inventory_manager.py
inventory_management_demo.py
Output

6. Quiz

Design Inventory Management System - Quiz

1 / 20
Multiple Choice

Which entity is primarily responsible for maintaining stock levels for each product at a specific location in an Inventory Management System?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script